在 Node.js 中使用 Promise.prototype.finally
(点击上方公众号,可快速关注)
英文: Valeri Karpov 译文:众成翻译/AlekoLau
zcfy.cc/article/using-promise-prototype-finally-in-node-js
Promise.prototype.finally() 最近达到了 TC39 提案的 第 4 阶段 。这意味着 Promise.prototype.finally() 提案被采纳成为 ECMAScript 最新特性草案 的一部分,登陆 Node.js 现在只是时间问题了。这篇文章会向大家展示 Promise.prototype.finally() 的用法和简化版 Polyfill 的写法。
Promise.prototype.finally() 是什么?
假设你创建了一个新的 Promise:
const promiseThatFulfills = new Promise((resolve) => {
// 调用 resolve() 可以让 Promised 的状态变为 fulfilled。"fulfilled" 和 "resolved" 是不同的概念:
// 如果你 resolve() 一个非 Promise 值,Promise 会变成 "fulfilled"。
// 然而, 如果 resolve() 一个 Promise,外层(原来的) Promise 会保持 "pending" 状态
// 直到内层 Promise 变为 "fulfilled" 或者 "rejected"
setTimeout(() => resolve('Hello, World'), 1000);
});
const promiseThatRejects = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('whoops!')), 1000);
});
你可以用 .then() 函数把这些 Promise 串联在一起。
promiseThatFulfills.then(() => console.log('Will print after about 1 second'));
promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));
注意 .then() 需要两个函数作为参数。第一个参数是 onFulfilled(),当 Promise 为 fulfilled 时调用;第二个 onRejected() 则是在 rejected 的时候调用。Promise 是一个必定处于以下三种状态之一的状态机:
pending(进行中): Promise 中的操作正在进行中,状态未被凝固为 fulfilled 或 rejected。
fulfilled(已完成,直译:已满足): Promise 中的操作已成功完成,现在 Promise 里面关联有该操作的返回值。
rejected(已失败,直译:已回绝): Promise 中的操作因某些原因失败,现在 Promise 里面关联有该操作的错误信息。
此外,处于 fulfilled 或者 rejected 状态的 Promise 称作“已凝固”(settled) 的 Promise。
虽然 .then() 是串联 Promise 的核心机制,但并不独一无二。Promise 用来处理抛出错误的 .catch() 函数 也能串联 Promise。
promiseThatRejects.catch(() => console.log('Will print after about 1 second'));
.catch() 函数只是一个只有 onRejected() 参数的 .then() 的语法糖:
promiseThatRejects.catch(() => console.log('Will print after about 1 second'));
// 等价于
promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));
类似于 .catch(),.finally() 也是 .then() 的一个语法糖。区别在于 .finally() 当 Promise 凝固(fulfilled / rejected)时执行一个 onFinally 函数。当前 .finally() 还没有加入 Node.js 发行版,但 npm 上的 promise.prototype.finally 模块 实现了它的 Polyfill。
const promiseFinally = require('promise.prototype.finally');
// 向 Promise.prototype 增加 finally()
promiseFinally.shim();
const promiseThatFulfills = new Promise((resolve) => {
setTimeout(() => resolve('Hello, World'), 1000);
});
const promiseThatRejects = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('whoops!')), 1000);
});
promiseThatFulfills.finally(() => console.log('fulfilled'));
promiseThatRejects.finally(() => console.log('rejected'));
上面代码的运行结果会打印 'fulfilled' 和 'rejected',因为无论是 fulfilled 还是 rejected,只要状态凝固 onFinally 都会立即执行。不过 onFinally 不接受参数,所以你无法判断 Promise 的状态到底是两个中的哪个。
finally() 会返回一个 Promise,所以你可以使用 .then() / .catch() / .finally() 串联它的返回值。finally() 返回的 Promise 会和它连接到的 Promise 保持相同的 fulfill 条件。 例如下面的代码,即使 onFinally 返回了 'bar',它还是会打印 5 次 'foo' 。
const promiseFinally = require('promise.prototype.finally');
// 向 Promise.prototype 增加 finally()
promiseFinally.shim();
Promise.resolve('foo').
finally(() => 'bar').
// 会打印 'foo', **不是** 'bar',因为 finally() 只起到转运的作用
// for fulfilled values and rejected errors
then(res => console.log(res));
类似地,下面代码中即使 onFinally 没有抛出任何错误,仍然会打印 'foo'。
const promiseFinally = require('promise.prototype.finally');
// 向 Promise.prototype 增加 finally()
promiseFinally.shim();
Promise.reject(new Error('foo')).
finally(() => 'bar').
// 会打印 'foo', **不是** 'bar',因为 finally() 只起到转运的作用
// 无论是 resolve 的值还是 reject 的错误
catch(err => console.log(err.message));
上面代码展示了使用 finally() 的一个重要细节:它 不会 帮你处理 Promise 的错误。如何让它能处理 Promise 错误值得更深入的研究。
错误处理
finally() 不是 用来处理 Promise 的错误的。事实上,它会在 onFinally() 执行后显式重新抛错。下面的代码会打印一个未被处理的 Promise 错误警告。
const promiseFinally = require('promise.prototype.finally');
// 向 Promise.prototype 增加 finally()
promiseFinally.shim();
const promiseThatRejects = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('whoops!')), 1000);
});
promiseThatRejects.finally(() => console.log('rejected'));
$ node finally.js
rejected
(node:5342) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: whoops!
(node:5342) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
$
与 try/catch/finally 类似,通常 .finally() 都会在 .catch() 后面被调用。
const promiseFinally = require('promise.prototype.finally');
// 向 Promise.prototype 增加 finally()
promiseFinally.shim();
const promiseThatRejects = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('whoops!')), 1000);
});
promiseThatRejects.
catch(() => { /* ignore the error */ }).
finally(() => console.log('done'));
然而 finally() 返回的也是 Promise,所以你可以随意在 finally() 后面调用 .catch()。特别地,如果 onFinally 会出错,例如 HTTP 请求,你应该在末尾添加 .catch() 以处理可能发生的错误。
const promiseFinally = require('promise.prototype.finally');
// 向 Promise.prototype 增加 finally()
promiseFinally.shim();
const promiseThatRejects = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('whoops!')), 1000);
});
promiseThatRejects.
finally(() => console.log('rejected')).
// No unhandled promise rejection because there's a .catch()
catch(() => { /* ignore the error */ });
简版 Polyfill
我觉得想要真正搞懂一个东西,最简单的方式就是自己去实现一个。.finally() 是一个很好的选择,因为官方 Polyfill 只有 45 行,而且大多数代码在验证原理时可以进一步精简。
接下来是一些关于 .finally() 的测试样例。下面的代码会打印 'foo' 5 次。
// 返回值被忽略,Promise 正常完成
Promise.resolve('foo').
finally(() => 'bar').
then(res => console.log(res));
// 返回值被忽略,Promise 正常抛错
Promise.reject(new Error('foo')).
finally(() => 'bar').
catch(err => console.log(err.message));
// onFinally 抛错,返回新抛出的错误
Promise.reject(new Error('bar')).
finally(() => { throw new Error('foo'); }).
catch(err => console.log(err.message));
// onFinally 返回的是一个抛错的 Promise,
// 返回新抛出的错误
Promise.reject(new Error('bar')).
finally(() => Promise.reject(new Error('foo'))).
catch(err => console.log(err.message));
// onFinally 返回的是一个 Promise, 需要等待它
// 状态凝固才能继续执行
const start = Date.now();
Promise.resolve('foo').
finally(() => new Promise(resolve => setTimeout(() => resolve(), 1000))).
then(res => console.log(res, Date.now() - start));
下面是简版 Polyfill 的实现。
// 向 Promise.prototype 增加 finally()
Promise.prototype.finally = function(onFinally) {
return this.then(
/* onFulfilled */
res => Promise.resolve(onFinally()).then(() => res),
/* onRejected */
err => Promise.resolve(onFinally()).then(() => { throw err; })
);
};
这个实现背后关键的思路在于 onFinally 可能返回 Promise。在这种情况下你需要用 .then() 来处理它并且给外层 Promise 凝固状态。你可以显式检查 onFinally 是否返回 Promise,但 Promise.resolve() 已经帮你做了,而且不需要 if 语句。你还需要跟踪初始 Promise 的值或错误,并确保 finally() 返回的 Promise 解析出初始值 res,或重新抛出初始错误 err。
后记
在动笔时,Promise.prototype.finally() 是 8 个 TC39 第四阶段提案 之一。这意味着 finally() 将和 7 个其他新语言特性一起加入 Node.js。 finally() 是这 8 个新特性中最令人兴奋的之一,皆因为它可以让异步操作结束后的清理更彻底。举个例子,下面我正用在生产环境的代码非常需要 finally() 来在函数完成时释放资源的锁定。
【关于投稿】
如果大家有原创好文投稿,请直接给公号发送留言。
① 留言格式:
【投稿】+《 文章标题》+ 文章链接
② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/
③ 最后请附上您的个人简介哈~
觉得本文对你有帮助?请分享给更多人
关注「前端大全」,提升前端技能